Dansk

En omfattende guide til TypeScript generics, der dækker deres syntaks, fordele, avanceret brug og bedste praksis for håndtering af komplekse datatyper i global softwareudvikling.

TypeScript Generics: Beherskelse af komplekse datatyper for robuste applikationer

TypeScript, et supersæt af JavaScript, giver udviklere mulighed for at skrive mere robust og vedligeholdelsesvenlig kode gennem statisk typning. Blandt dets mest kraftfulde funktioner er generics, som giver dig mulighed for at skrive kode, der kan arbejde med en række forskellige datatyper, samtidig med at typesikkerheden bevares. Denne guide giver en omfattende udforskning af TypeScript generics med fokus på deres anvendelse på komplekse datatyper i konteksten af global softwareudvikling.

Hvad er Generics?

Generics giver en måde at skrive genanvendelig kode, der kan arbejde med forskellige typer. I stedet for at skrive separate funktioner eller klasser for hver type, du vil understøtte, kan du skrive en enkelt funktion eller klasse, der bruger typeparametre. Disse typeparametre er pladsholdere for de faktiske typer, der vil blive brugt, når funktionen eller klassen kaldes eller instantieres. Dette er især nyttigt, når man arbejder med komplekse datastrukturer, hvor typen af data inden i disse strukturer kan variere.

Fordele ved at bruge Generics

Grundlæggende syntaks for Generics

Den grundlæggende syntaks for generics involverer brugen af vinkelparenteser (< >) til at erklære typeparametre. Disse typeparametre navngives typisk T, K, V, osv., men du kan bruge enhver gyldig identifikator. Her er et simpelt eksempel på en generisk funktion:


function identity<T>(arg: T): T {
  return arg;
}

let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);

console.log(myString); // Output: hello
console.log(myNumber); // Output: 123
console.log(myBoolean); // Output: true

I dette eksempel erklærer <T> en typeparameter ved navn T. Funktionen identity tager et argument af typen T og returnerer en værdi af typen T. Når du kalder funktionen, kan du eksplicit angive typeparameteren (f.eks. identity<string>) eller lade TypeScript udlede den baseret på argumentets type.

Arbejde med komplekse datatyper

Generics bliver særligt værdifulde, når man arbejder med komplekse datatyper som arrays, objekter og interfaces. Lad os udforske nogle almindelige scenarier:

Generiske Arrays

Du kan bruge generics til at oprette funktioner eller klasser, der arbejder med arrays af forskellige typer:


function arrayToString<T>(arr: T[]): string {
  return arr.join(", ");
}

let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];

console.log(arrayToString(numberArray)); // Output: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Output: apple, banana, cherry

Her tager funktionen arrayToString et array af typen T[] og returnerer en strengrepræsentation af arrayet. Denne funktion virker med arrays af enhver type, hvilket gør den yderst genanvendelig.

Generiske Objekter

Generics kan også bruges til at definere funktioner eller klasser, der arbejder med objekter af forskellige former:


interface Person {
  name: string;
  age: number;
  country: string; // Tilføjet land for global kontekst
}

interface Product {
  id: number;
  name: string;
  price: number;
  currency: string; // Tilføjet valuta for global kontekst
}

function displayInfo<T extends { name: string }>(item: T): void {
  console.log(`Name: ${item.name}`);
}

let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };

displayInfo(person); // Output: Name: Alice
displayInfo(product); // Output: Name: Laptop

I dette eksempel tager funktionen displayInfo et objekt af typen T, som skal have en name-egenskab af typen string. Klausulen extends { name: string } er en begrænsning, som specificerer minimumskravene for typeparameteren T. Dette sikrer, at funktionen sikkert kan tilgå name-egenskaben.

Avanceret brug af Generics

TypeScript generics tilbyder mere avancerede funktioner, der giver dig mulighed for at skabe endnu mere fleksibel og kraftfuld kode. Lad os udforske nogle af disse funktioner:

Flere typeparametre

Du kan definere funktioner eller klasser med flere typeparametre:


function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

interface Name {
  firstName: string;
}

interface Age {
  age: number;
}

const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };

const merged = merge(person, details);
console.log(merged.firstName); // Output: Bob
console.log(merged.age); // Output: 42

Funktionen merge tager to objekter af typerne T og U og returnerer et nyt objekt, der indeholder egenskaberne fra begge objekter. Dette er en kraftfuld måde at kombinere data fra forskellige kilder på.

Generiske begrænsninger

Som vist tidligere giver begrænsninger dig mulighed for at indskrænke de typer, der kan bruges med en generisk typeparameter. Dette sikrer, at den generiske kode sikkert kan operere på de specificerede typer.


interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

loggingIdentity([1, 2, 3]); // Output: 3
loggingIdentity("hello"); // Output: 5
// loggingIdentity(123); // Fejl: Argument af typen 'number' kan ikke tildeles til parameter af typen 'Lengthwise'.

Funktionen loggingIdentity tager et argument af typen T, som skal have en length-egenskab af typen number. Dette sikrer, at funktionen sikkert kan tilgå length-egenskaben.

Generiske klasser

Generics kan også bruges med klasser:


class DataStorage<T> {
  private data: T[] = [];

  addItem(item: T) {
    this.data.push(item);
  }

  removeItem(item: T) {
    this.data = this.data.filter(d => d !== item);
  }

  getItems(): T[] {
    return [...this.data];
  }
}

const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Output: [ 'banana' ]

const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Output: [ 2 ]

Klassen DataStorage kan opbevare data af enhver type T. Dette giver dig mulighed for at oprette genanvendelige datastrukturer, der er typesikre.

Generiske interfaces

Generiske interfaces er nyttige til at definere kontrakter, der kan arbejde med forskellige typer. For eksempel:


interface Result<T, E> {
  success: boolean;
  data?: T;
  error?: E;
}

interface User {
  id: number;
  username: string;
  email: string;
}

interface ErrorMessage {
  code: number;
  message: string;
}

function fetchUser(id: number): Result<User, ErrorMessage> {
  if (id === 1) {
    return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
  } else {
    return { success: false, error: { code: 404, message: "User not found" } };
  }
}

const userResult = fetchUser(1);
if (userResult.success) {
  console.log(userResult.data.username);
} else {
  console.log(userResult.error.message);
}

Interfacet Result definerer en generisk struktur til at repræsentere resultatet af en operation. Det kan enten indeholde data af typen T eller en fejl af typen E. Dette er et almindeligt mønster til håndtering af asynkrone operationer eller operationer, der kan mislykkes.

Utility Typer og Generics

TypeScript leverer flere indbyggede utility-typer, der fungerer godt med generics. Disse utility-typer kan hjælpe dig med at transformere og manipulere typer på kraftfulde måder.

Partial<T>

Partial<T> gør alle egenskaber af typen T valgfrie:


interface Person {
  name: string;
  age: number;
}

type PartialPerson = Partial<Person>;

const partialPerson: PartialPerson = { name: "Alice" }; // Gyldig

Readonly<T>

Readonly<T> gør alle egenskaber af typen T skrivebeskyttede:


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;

const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Fejl: Kan ikke tildele til 'age', da det er en skrivebeskyttet egenskab.

Pick<T, K>

Pick<T, K> vælger et sæt af egenskaber K fra typen T:


interface Person {
  name: string;
  age: number;
  email: string;
}

type NameAndAge = Pick<Person, "name" | "age">;

const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };

Omit<T, K>

Omit<T, K> fjerner et sæt af egenskaber K fra typen T:


interface Person {
  name: string;
  age: number;
  email: string;
}

type PersonWithoutEmail = Omit<Person, "email">;

const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };

Record<K, T>

Record<K, T> opretter en type med nøgler K og værdier af typen T:


type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Udvidet liste for global kontekst
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Udvidet liste for global kontekst

type CurrencyMap = Record<CountryCodes, Currency>;

const currencyMap: CurrencyMap = {
  "US": "USD",
  "CA": "CAD",
  "UK": "GBP",
  "DE": "EUR",
  "FR": "EUR",
  "JP": "JPY",
  "CN": "CNY",
  "IN": "INR",
  "BR": "BRL",
  "AU": "AUD",
};

Mapped Typer

Mapped typer giver dig mulighed for at transformere eksisterende typer ved at iterere over deres egenskaber. Dette er en kraftfuld måde at skabe nye typer baseret på eksisterende. For eksempel kan du oprette en type, der gør alle egenskaber af en anden type skrivebeskyttede:


interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = {
  readonly [K in keyof Person]: Person[K];
};

const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Fejl: Kan ikke tildele til 'age', da det er en skrivebeskyttet egenskab.

I dette eksempel itererer [K in keyof Person] over alle nøglerne i Person-interfacet, og Person[K] tilgår typen af hver egenskab. Nøgleordet readonly gør hver egenskab skrivebeskyttet.

Betingede Typer

Betingede typer giver dig mulighed for at definere typer baseret på betingelser. Dette er en kraftfuld måde at skabe typer, der tilpasser sig forskellige scenarier.


type NonNullable<T> = T extends null | undefined ? never : T;

type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string

function getValue<T>(value: T): NonNullable<T> {
  if (value == null) { // Håndterer både null og undefined
    throw new Error("Value cannot be null or undefined");
  }
  return value as NonNullable<T>;
}

try {
  const validValue = getValue("hello");
  console.log(validValue.toUpperCase()); // Output: HELLO

  const invalidValue = getValue(null); // Dette vil kaste en fejl
  console.log(invalidValue); // Denne linje vil ikke blive nået
} catch (error: any) {
  console.error(error.message); // Output: Value cannot be null or undefined
}

I dette eksempel kontrollerer typen NonNullable<T>, om T er null eller undefined. Hvis det er tilfældet, returnerer den never, hvilket betyder, at typen ikke er tilladt. Ellers returnerer den T. Dette giver dig mulighed for at oprette typer, der garanteret ikke er nullable.

Bedste praksis for brug af Generics

Her er nogle bedste praksis, du skal huske på, når du bruger generics:

Eksempler i en global kontekst

Lad os se på nogle eksempler på, hvordan generics kan bruges i en global kontekst:

Valutakonvertering


interface ConversionRate {
  rate: number;
  fromCurrency: string;
  toCurrency: string;
}

function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
  return amount * rate.rate;
}

const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD is equal to ${amountInEUR} EUR`); // Output: 100 USD is equal to 85 EUR

Datoformatering


interface DateFormatOptions {
  locale: string;
  options: Intl.DateTimeFormatOptions;
}

function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
  return date.toLocaleDateString(format.locale, format.options);
}

const currentDate = new Date();

const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };

console.log("US Date: " + formatDate(currentDate, usDateFormat));
console.log("German Date: " + formatDate(currentDate, germanDateFormat));
console.log("Japanese Date: " + formatDate(currentDate, japaneseDateFormat));

Oversættelsestjeneste


interface Translation {
  [key: string]: string; // Tillader dynamiske sprognøgler
}

interface LanguageData<T extends Translation> {
  languageCode: string;
  translations: T;
}

const englishTranslations: Translation = {
  "hello": "Hello",
  "goodbye": "Goodbye",
  "welcome": "Welcome to our website!"
};

const spanishTranslations: Translation = {
  "hello": "Hola",
  "goodbye": "Adiós",
  "welcome": "¡Bienvenido a nuestro sitio web!"
};

const frenchTranslations: Translation = {
  "hello": "Bonjour",
  "goodbye": "Au revoir",
  "welcome": "Bienvenue sur notre site web !"
};


const languageData: LanguageData<typeof englishTranslations>[] = [
  {languageCode: "en", translations: englishTranslations },
  {languageCode: "es", translations: spanishTranslations },
  {languageCode: "fr", translations: frenchTranslations}
];

function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
  const lang = languageData.find(lang => lang.languageCode === languageCode);
  if (!lang) {
    return `Translation for ${key} in ${languageCode} not found.`;
  }
  return lang.translations[key] || `Translation for ${key} not found.`;
}

console.log(translate("hello", "en", languageData)); // Output: Hello
console.log(translate("hello", "es", languageData)); // Output: Hola
console.log(translate("welcome", "fr", languageData)); // Output: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Output: Translation for missingKey in de not found.

Konklusion

TypeScript generics er et kraftfuldt værktøj til at skrive genanvendelig, typesikker kode, der kan arbejde med komplekse datatyper. Ved at forstå den grundlæggende syntaks, avancerede funktioner og bedste praksis for generics kan du betydeligt forbedre kvaliteten og vedligeholdelsen af dine TypeScript-applikationer. Når du udvikler applikationer til et globalt publikum, kan generics hjælpe dig med at håndtere forskellige dataformater og kulturelle konventioner, hvilket sikrer en problemfri brugeroplevelse for alle.